package com.qire.manhua.view.comicReader;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;

import com.qire.common.function.Consumer;
import com.qire.manhua.view.comicReader.renderer.PictureResource;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * 图片阅读器：<br />
 * 图片阅读器内部维护了一个 pictureSafeList 安全容器列表，用于存储所有的图片资源；
 * 但用于绘制的只是其中一部分图片，这部分图片会超出屏幕大小以确保滚动时不出现黑屏。
 * 当阅读器滚动时会触发滑动偏移位置检测，来更新 drawStartIndex （绘制起点索引）,
 * 并从起点处向下取 drawLength (绘制长度) 张图片用于绘制，没有在这个范围的图片资源会被释放回收，以避免OOM及资源浪费。
 * 而加载到的图片则会在绘制时从资源池中读取，资源池会自动统一的管理所有图片的销毁及释放。
 * 由于期望始终保持绘制的图片内容呢始终超过屏幕的上下边框，故需要保证 drawStartIndex 始终在现实内容索引的前2个图片开始绘制，
 * 而 drawLength 长度能超过满屏绘制的图片还多，这样可以避免滑动时出现衔接处黑屏。
 *
 * <pre>
 * pictureSafeList
 * 	 +-------+
 * 	 |   0   |
 * 	 +-------+
 * 	 |   1   |                       drawList         drawList超出部分
 * 	 +-------+  ← drawStartIndex →  +-------+           _________
 * 	 |   2   |                      |   2   |          |         | 手
 * 	 +-------+                      +-------+          |         | 机
 * 	 |   3   |                      |   3   | → draw → |         | 屏
 * 	 +-------+    ← drawLength →    +-------+          |         | 幕
 * 	 |   4   |                      |   4   |          |_________|
 * 	 +-------+                      +-------+         drawList超出部分
 * 	 |  ...  |
 * 	 +-------+
 * </pre>
 *
 */
public class PictureReaderView extends View {

	/** 绘制滤镜：图片优化过滤器，用于处理图片清晰度问题，利用插值算法处理缩放后图片绘制不清晰的问题 */
	private final PaintFlagsDrawFilter pfd = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

	/** 图片资源容器 */
	private final PictureSafeList pictureSafeList = new PictureSafeList();

	/** 绘制图片长度 */
	private final int drawLength = 8;

	/** 可见图片资源数组 */
	private final PictureResource[] visibleResourceArray = new PictureResource[drawLength];

	/** 绘制图片起始索引 */
	private int drawStartIndex = -1;

	/** 滑动位置X, Y 坐标 */
	private int mScrollX = 0, mScrollY = 0;

	/** 滑动方向：向顶部滑动, 向底部滑动  */
	private boolean isToTheTop = false, isToTheBottom = false;

	/** 绘制调度器，用来同步绘制刷新频率 */
	protected DrawScheduler drawScheduler;

	public PictureReaderView(Context context) {
		super(context);
	}

	public PictureReaderView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public PictureReaderView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}

	/**
	 * 滑动时触发
	 * @param x 当前滑动到的x位置
	 * @param y 当前滑动到的y位置
	 * @param maxX 最大可滑动到的x位置
	 * @param maxY 最大可滑动到的y位置
	 */
	protected void onScroll(int x, int y, int maxX, int maxY) {
		isToTheTop = false;
		isToTheBottom = false;
		if(y >= 0) {
			isToTheTop = true;
		}
		if(y <= -maxY) {
			isToTheBottom = true;
		}
	}

	public Point getScroll() {
		return new Point(mScrollX, mScrollY);
	}

	public boolean isToTheTop() {
		return isToTheTop;
	}

	public boolean isToTheBottom() {
		return isToTheBottom;
	}

	@Override
	public void scrollBy(int x, int y) {
		scrollTo(mScrollX + x, mScrollY + y);
	}

	@Override
	public void scrollTo(int x, int y) {
		final PictureResource lastResource = pictureSafeList.last();
		// 如果列表的最后一个资源不为空
		if(lastResource != null) {
			int maxY;
			Rect lastResourceRect = lastResource.calcViewportRect();
			if(lastResourceRect != null) {
				// 如果存在最后一个图片资源且资源可以绘制就以资源视口大小来计算可滑动的最大距离
				maxY = Math.max(lastResourceRect.bottom - getHeight(), 0);
			} else {
				// 否则取控件的大小
				maxY = getHeight();
			}

			// 处理Y滑动后超过可滑动范围的情况
			y = Math.min(y, 0);
			y = Math.max(y, -maxY);

			// 设置滑动坐标
			if (mScrollX != x || mScrollY != y) {
				mScrollX = x;
				mScrollY = y;
				locateVisibleResources(mScrollX, mScrollY);
				requestInvalidate();
			}

			// 通知扩展滑动事件
			onScroll(x, y, getWidth(), maxY);
		}
	}

	protected void onLocateResources(PictureResource pictureResource, int comicHeight) {

	}

	/**
	 * 通过滑动坐标 {@code scrollX, scrollY} 定位可见图片资源用于绘制相关操作。
	 * @param scrollX 滑动到的X坐标
	 * @param scrollY 滑动到的Y坐标
	 */
	private void locateVisibleResources(final int scrollX, final int scrollY) {
		// 获得最后一个节点资源，如果为null说明列表可能为空，丢弃本次移动刷新，由于这个可能触发多次所以丢弃也没有太多问题
		PictureResource lastResource = pictureSafeList.last();
		if(lastResource == null) {
			return;
		}

		// 为了记录人气值上报当前章节漫画查看了多少页，获取整个漫画长度。
		int comicHeight = lastResource.calcViewportRect().bottom;

		// 从漫画资源列表中通过滑动到的坐标位置X,Y定位出所处图片资源的序号，用于构建可见绘制资源列表。
		int startIndex = pictureSafeList.indexOf(resource -> {
			if(resource.viewportContains(scrollX, -scrollY)) {
				// todo 可以考虑是否通过其他方式完成统计，通知子View定位到的资源，
				onLocateResources(resource, comicHeight);
				return true;
			}
			return false;
		});

		// startIndex -1 表示未找到坐标对应的资源， >=0 则表示坐标对应资源的索引位置。
		if(startIndex >= 0) {
			// 为了保证滑动位置所对资源，在可见链表的中间，故整个列表的查找需要上移2个资源，以保证可见绘制列表能够在上下都超屏覆盖
			startIndex = startIndex > 2 ? (startIndex - 2) : startIndex;

			// 过滤索引位置是否在合理位置内，因为会显示 drawLength 长度，所以防止列表取值超范围 取 0~(size - drawLength)内
			// 由于重构后绘制做了判空处理其实可以不需要一定限制在这个范围内，只要保证 index 不会再listSize之外就可以了。这点可以通过 indexOf 保证。下面可删除
			startIndex = Math.min(startIndex, pictureSafeList.size() - drawLength);
			startIndex = Math.max(startIndex, 0);

			// 索引是否发生改变，如果改变则替换索引，并且更新可见资源
			if(drawStartIndex != startIndex) {
				pictureSafeList.fillRange(visibleResourceArray, startIndex, drawLength);
				drawStartIndex = startIndex;
			}
		}
	}

	@Override
	public void onDraw(Canvas canvas) {
		// 清理画面，设置背景
//		canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
		canvas.drawColor(Color.rgb(0x34,0x34,0x34));

		onDrawVisible(canvas, mScrollX, mScrollY, visibleResourceArray);
	}

	/**
	 * 绘制可见资源，为了更好的管理可见资源 visiblePictureResources 对子类进行约束，所以抽取 onDrawVisible 方法，这也有利于资源绘制的逻辑与顺序梳理。
	 * 其他任何地方只能通过这个方法才能获得 visiblePictureResources 的引用，所以子类可以实现这个方法来完成对 visiblePictureResources 的操作。
	 *
	 * @param canvas 画布
	 * @param scrollX 滑动X坐标，因为是值传递，所以这里并不会收到多线程影响
	 * @param scrollY 滑动Y坐标，因为是值传递，所以这里并不会收到多线程影响
	 * @param visibleResources 可见资源数组。
	 */
	protected void onDrawVisible(Canvas canvas, int scrollX, int scrollY, PictureResource[] visibleResources) {
		canvas.save();
		canvas.translate(scrollX, scrollY);
		canvas.setDrawFilter(pfd);
		boolean needRedraw = false;
		for(PictureResource resource : visibleResources) {
			if(resource != null && resource.calcViewportRect() != null) {
				if(!resource.draw(canvas)) {
					// 绘制失败
					needRedraw = true;
				}
			}
		}
		if(needRedraw) {
			requestInvalidate();
		}
		canvas.restore();
	}

	@Override
	protected void onDetachedFromWindow() {
		pictureSafeList.clear();
		super.onDetachedFromWindow();
	}

	/** 请求重绘，如果开启线程调度模式则忽略线程调度会自己刷新，如果禁用线程调度，则主动重绘 */
	public void requestInvalidate() {
		if(drawScheduler == null) {
			invalidate();
		}
	}

	/** 启用绘制调度模式，之后的绘制由子线程接管，按固定频率来刷新绘制， */
	public synchronized void enableDrawScheduler() {
		if(drawScheduler == null) {
			drawScheduler = new DrawScheduler(this);
			drawScheduler.start();
		}
	}

	/** 禁用绘制调度模式，之后的绘制需要自己主动触发 */
	public synchronized void disableDrawScheduler() {
		if(drawScheduler != null) {
			drawScheduler.exit();
			drawScheduler = null;
		}
	}

	private void resetDraw() {
		Arrays.fill(visibleResourceArray, null);
		drawStartIndex = -1;
		mScrollX = -1;
		mScrollY = -1;
		scrollTo(0, 0);
	}

	/**
	 * 如果允许，则更换图片资源列表内容:
	 * 1.新资源列表不为空
	 * 2.新资源列表于老资源列表不同，由于资源列表属于固定且有序，所以比较大小与首尾资源不同即可
	 * @param resourceList 新资源列表
	 */
	public void loadOrReplace(ArrayList<PictureResource> resourceList) {
		if(pictureSafeList.allowReplace(resourceList)) {
			pictureSafeList.forEach((PictureResource pictureResource) -> pictureResource.recycled());
			pictureSafeList.clear();
			pictureSafeList.addAll(resourceList);
			resetDraw();
		}
	}

	/**
	 * 停止所有的资源数据加载，如果存在资源隐射的图片在任务队列里下载的话。
	 */
	public void stopAllLoad() {
		pictureSafeList.forEach(this::stopLoad);
	}

	/**
	 * 停止指定资源的数据加载
	 * @param pictureResource
	 */
	public void stopLoad(PictureResource pictureResource) {
		pictureResource.stopLoad();
	}

	/**
	 * 提供给子类的一个遍历所有资源的方法
	 * @param action 期望遍历后处理的行为
	 */
	protected void forEachResource(Consumer<PictureResource> action) {
		pictureSafeList.forEach(action);
	}

	protected PictureResource findResourceBy(final int x, final int y) {
		return pictureSafeList.findBy(resource -> resource.viewportContains(x, y));
	}

	/**
	 * 绘制调度器，让多个触发刷新源处于同一个频率来触发刷新。
	 */
	private class DrawScheduler extends Thread {
		/**
		 * 基本流畅：24帧/s (41 ms/次),
		 * 平滑流畅：60帧/s (16 ms/次)
		 * 一般显示器的频率60帧/s;设置区间应该在 16~41 ms/次
		 */
		private final long RefreshRate = 16;

		private final PictureReaderView readerView;

		private long mLastLockTime = 0;

		private volatile boolean isRun = true;

		private DrawScheduler(PictureReaderView readerView) {
			this.readerView = readerView;
			setName("readerView 绘制调度线程：" + readerView.toString());
		}

		private void exit() {
			isRun = false;
		}

		@Override
		public void run() {
			while(isRun) {
				readerView.postInvalidate();
				sleep();
			}
			System.out.println(getName() + "线程已退出");
		}

		private void sleep() {
			long now = SystemClock.uptimeMillis();
			long nextTime = mLastLockTime + RefreshRate;
			if (nextTime > now) {
				try {
					Thread.sleep(nextTime - now);
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
				now = SystemClock.uptimeMillis();
			}
			mLastLockTime = now;
		}
	}

}
